与非C++代码的连接
在许多情况下,一个C++程序中也可能包含着一些采用其他语言写出的部分。类似地,C++代码片段也经常被用于主要由其他语言写出的程序。用不同语言写出的程序片段之间的协作比较困难;甚至采用同一种语言写出,但通过不同编译器编译的片段也是这样。例如,不同语言,或者同一语言的不同实现可能在它们使用寄存器保存参数的方式上,在将参数放入堆栈的顺序上,在整数或字符串等内部类型的布局上,在编译器传递给连接器的名字方面,在对连接器所要求的类型检查的量等方面存在着差异。为了能有所帮助,可以在一个extern声明中给出有关的连接约定。例如,下面声明了C和C++标准库函数strcpy(),并特别说明它应该按照C连接约定进行连接:
extern "C" char* strcpy(char*, const char*);
这个声明的作用与
extern char* strcpy(char*, const char*);
“简单”声明的差异仅在于调用strcpy()的连接约定不同。
由于C和C++语言之间的紧密关系,extern "C"指令就特别有用。请注意,在extern "C"中的C表示一个连接约定,而不是一种语言。人们也经常将extern "C"用于连接Fortran和汇编例程,因为它们的要求也正好符合C实现的约定。
extern "C"指令描述的(只)是一种连接约定,它并不影响调用函数的语义。特别地,声明为extern "C"的函数仍然要遵守C++的类型检查和参数转换规则,而不是C的较弱的规则。例如,
extern "C" int f();
int g()
{
return f(1); // 错误❌:未期望有参数
}
为许多声明添加extern "C"也会成为令人讨厌的事情。因此,这里还提供了一种为一组声明描述连接约定的机制。例如,
extern "C" {
char* strcpy (char*, const char*);
int strcmp (const char*, const char*);
int strlen (cosnt char*);
// ...
}
这种结构经常被称为连接块,它可以用于包裹起整个的C头文件,使整个文件能适合C++的使用。例如,
extern "C" {
#include <string.h>
}
这种技术经常被用于由C头文件产生出的C++头文件。换一种方式,也可以用条件编译(7.8.1节)建立起公共的C和C++头文件:
#ifdef __cplusplus
extern "C" {
#endif
char* strcpy(char*, const char*);
int strcmp(const char*, const char*);
int strlen(const char*);
// ...
#ifdef __cplusplus
}
#endif
__cplusplus是一个预定义的宏名字,它被用于保证当这个文件被用做C头文件时,其中的C++结构将被去掉。
任何声明都可以出现在连接块里:
extern "C" { // 这里可以有任何声明,如:
int g1; // 定义
extern int g2; // 声明,不是定义
}
应特别指出,变量的作用域及存储类都不会受到影响。所以,g1仍然是一个全局变量,还是在这里被定义而不是被声明。要想声明而不是定义一个变量,你必须将关键字extern直接应用在声明里。例如,
extern "C" int g3; // 声明,不是定义
这一写法初看起来有点古怪。然而,它不过是给一个外部声明加上“C”后应该保持意义不变,将一个文件用连接块包裹后意义也不变的一个简单推论。
有着C连接的名字也可以在名字空间里定义。名字空间只影响到C++程序里对于该名字的访问方式,但却不会影响连接器看到它的方式。取自std的printf()是一个典型的例子:
#include <cstdio>
void f()
{
std::printf("Hello, "); // ok
printf("world!\n"); // 错误❌:没有全局的printf()
}
即使调用的是std::printf(),它也还是那个老的C的printf()(21.8节)。
注意,这些将使我们可以把具有C连接的库包含到选定的名字空间里,而不去污染全局名字空间。不幸的是,对于将具有C++连接的函数定义在全局名字空间里的头文件,我们就没有同样的灵活手段了。出现此问题,是因为C++实体的连接必须将名字空间考虑在内,因此所生成的目标文件将反应它们在或者不在名字空间里的情况。
🔚